package com.ipan.jfinal.simpleSso;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

import com.ipan.kits.number.RandomUtil;
import com.ipan.kits.security.DigestUtil;
import com.ipan.kits.time.DateFormatUtil;

/**
 * 单点登录数据包装器
 * 
 * 客户端，使用create()方法去创建一个单例供调用；
 * 服务端，使用static方法去校验；每个单点登录接入的客户端的配置是不一样的；
 * 
 * 前端提交参数：
 * appid		【请求】系统分配；
 * userid		【请求】系统分配；
 * 
 * username		【请求】请求参数，第三方系统用户名；
 * timestamp	【请求】请求参数，第三方系统时间，格式yyyyMMddHHmmssSSS；
 * nonce		【请求】请求参数，随机数8-32位（数字）；
 * 
 * appkey		【自动获取】系统分配，不需要前端提交，后端自动根据appid、userid获取appkey；
 * signature	【自动生成】根据请求参数，自动生成签名；
 * 
 * 注意，特殊键值不能在业务里使用：appid、userid、appkey、signature；
 * 注意，参数提交的key不要使用重复的key，要提交数组使用“,"分隔或使用Json字符串，避免使用同一个key重复提交；
 * 
 * @author iPan
 * @date 2022-07-20
 */
public class SimpleSSOPacker {
	
	public static final String APPID_NAME = "appid";
	public static final String USERID_NAME = "userid";
	public static final String APPKEY_NAME = "appkey";
	public static final String TIMESTAMP_NAME = "timestamp";
	public static final String NONCE_NAME = "nonce";
	public static final String SIGNATURE_NAME = "signature";
	
	private static final String[] EXCLUDE_KEY = {"signature"};
	private String appid = null;
	private String userid = null;
	private String appkey = null;
	private long offset = 99;
	
	/**
	 * 创建数据包装器
	 */
	public static SimpleSSOPacker create(String appid, String userid, String appkey) {
		return new SimpleSSOPacker(appid, userid, appkey);
	}
	/**
	 * 创建数据包装器（推荐）
	 */
	public static SimpleSSOPacker create(String appid, String userid, String appkey, long offset) {
		return new SimpleSSOPacker(appid, userid, appkey, offset);
	}
	
	private SimpleSSOPacker() {}
	private SimpleSSOPacker(String appid, String userid, String appkey) {
		this.appid = appid;
		this.userid = userid;
		this.appkey = appkey;
	}
	private SimpleSSOPacker(String appid, String userid, String appkey, long offset) {
		this.appid = appid;
		this.userid = userid;
		this.appkey = appkey;
		this.offset = offset;
	}

	// 是否排除的key
	private static boolean isExcludeKey(String key) {
		if (EXCLUDE_KEY.length < 1) {
			return false;
		}
		boolean ret = false;
		for (String s : EXCLUDE_KEY) {
			if (s.equals(key)) {
				ret = true;
				break;
			}
		}
		return ret;
	}
	
	public static String getTimestamp() {
		return DateFormatUtil.formatDate("yyyyMMddHHmmssSSS", new Date());
	}
	
	public static String getNonce() {
		return RandomUtil.nextLong(100000000, 999999999) + "";
	}
	/**
	 * 随机数带偏移
	 * 
	 * 带偏移量会更有迷惑性
	 * 
	 * @param nonce 随机数
	 * @param offset 偏移量offset都会被转为负数，0则直接返回原数字；
	 * @return 偏移后的数字
	 */
	public static String shiftNonce(String nonce, long offset) {
		if (offset == 0) return nonce; 
		int len = nonce.length();
		long pv = (offset > 0) ? -1 * offset : offset;
		long num = Long.parseLong(nonce) + pv;
		return StringUtils.leftPad(num + "", len, "0");
	}
	
	/**
	 * 参数拼接
	 * 键值，支持String、String[]、Number；
	 * 
	 * @param params 参数
	 * @param autoInject 是否自动注入必填字段
	 * @param isOffset 随机数是否偏移
	 */
	private static String paramsToStr(Map<String, Object> params, boolean isOffset, String appkey, long offset) {
		if (params == null || params.size() < 1) {
			return null;
		}
		
		Map<String, Object> tmpMap = new HashMap<String, Object>(params);
		tmpMap.put(APPKEY_NAME, appkey);
		List<PairParams> list = new ArrayList<PairParams>();
		Iterator<String> iter = tmpMap.keySet().iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			if (isExcludeKey(key)) {
				continue;
			}
			
			Object value = tmpMap.get(key);
			if (value == null) { // 值为null跳过
				continue;
			}
			
			if (NONCE_NAME.equals(key) && isOffset) { // 随机数带偏移量
				String nonceValue = getParam(params, NONCE_NAME);
				String offsetValue = shiftNonce(nonceValue, offset);
				list.add(new PairParams(key, new String[]{(String)offsetValue}));
				continue;
			}
			
			if (value instanceof String) {
				list.add(new PairParams(key, new String[]{(String)value}));
			} else if (value instanceof String[]) {
				Arrays.sort((String[]) value); // 默认排序，key同名的value升序；
				list.add(new PairParams(key, (String[]) value));
			} else if (value instanceof Number) {
				list.add(new PairParams(key, new String[]{value.toString()}));
			} else {
				throw new IllegalArgumentException("参数不规范");
			}
		}
		// 根据key升序
		Collections.sort(list); // 注意：key不要使用一样的key作为参数！
		StringBuilder buf = new StringBuilder();
		for (PairParams pp : list) {
			buf.append(pp.valueToString());
		}
		return buf.toString();
	}
	
	/**
	 * 生成加密字符串 
	 */
	public static String encrypt(Map params, boolean autoInject, boolean isOffset, 
			String appid, String userid, String appkey, long offset) {
		// 自动注入必填字段
		if (autoInject) {
			params.put(APPID_NAME, appid);
			params.put(USERID_NAME, userid);
			params.put(TIMESTAMP_NAME, getTimestamp());
			params.put(NONCE_NAME, getNonce());
		}
		String str = paramsToStr(params, isOffset, appkey, offset);
//		System.out.println("str=" + str);
		String sign = DigestUtil.sha256Hex(str);
		if (autoInject) {
			params.put(SIGNATURE_NAME, sign);
		}
		return sign;
	}
	public String encrypt(Map params) {
		return encrypt(params, true, true, this.appid, this.userid, this.appkey, this.offset);
	}
	public String encrypt(Map params, boolean autoInject, boolean isOffset) {
		return encrypt(params, autoInject, isOffset, this.appid, this.userid, this.appkey, this.offset);
	}
	
	/**
	 * 校验签名是否正确
	 * 
	 * @param params 参数（已经提交了appid、userid、timestamp、nonce）
	 * @param autoInject 是否自动注入
	 * @param isOffset 是否偏移
	 * @return true 合格 false 不合格
	 */
	public static boolean validate(Map params, boolean isOffset, 
			String appid, String userid, String appkey, long offset) {
		String sign = encrypt(params, false, isOffset, appid, userid, appkey, offset);
		String signOld = getParam(params, SIGNATURE_NAME);
		if (StringUtils.isBlank(signOld)) {
			return false;
		}
		return signOld.equals(sign);
	}
	public boolean validate(Map params, boolean isOffset) {
		return validate(params, isOffset, this.appid, this.userid, this.appkey, this.offset);
	}
	public boolean validate(Map params) {
		return validate(params, true, this.appid, this.userid, this.appkey, this.offset);
	}
	
	public static boolean validateTimestamp(String timestamp, int second) throws ParseException {
		Date d = DateFormatUtil.parseDate("yyyyMMddHHmmssSSS", timestamp);
		long diffVal = Math.abs(System.currentTimeMillis() - d.getTime());
//		System.out.println(diffVal);
		return diffVal < second * 1000;
	}
	
	private static String getParam(Map params, String name) {
		String retStr = null;
		Object value = params.get(name);
		if (value == null) {
			return null;
		}
		if (value instanceof String) {
			retStr = (String)value;
		} else if (value instanceof String[]) {
			String[] tmpVal = ((String[])value);
			retStr = (tmpVal.length > 0) ? tmpVal[0] : "";
		} else if (value instanceof Number) {
			retStr = value.toString();
		}
		return retStr;
	}
	
//	public static void main(String[] args) {
//		SimpleSSOPacker ssoPacker = SimpleSSOPacker.create("sso", "41650BC4DB315E1360C4C53242A95E89", "2B4A67387D22C53454225C675376614A", 100);
//		Map<String, Object> params = new HashMap<String, Object>();
//		params.put("username", "user1");
//		params.put("appid", "sso");
//		params.put("userid", "41650BC4DB315E1360C4C53242A95E89");
//		params.put("timestamp", "20220726174500111");
//		params.put("nonce", "100000000");
//		params.put("appkey", "2B4A67387D22C53454225C675376614A");
//		String sign = ssoPacker.encrypt(params, false, true);
//		System.out.println(sign);
//		params.put(SIGNATURE_NAME, sign);
//		System.out.println(ssoPacker.validate(params));
//		System.out.println(validate(params, true, "sso", "41650BC4DB315E1360C4C53242A95E89", "2B4A67387D22C53454225C675376614A", 100));
//	}

}

// 键值对字符串拼接
class PairParams implements Comparable<PairParams> {
	private String key;
	private String[] value; // String or String[]
	
	public PairParams(String key, String[] value) {
		this.key = key;
		this.value = value;
	}
	public String getKey() {
		return key;
	}
	public void setKey(String key) {
		this.key = key;
	}
	public String[] getValue() {
		return value;
	}
	public void setValue(String[] value) {
		this.value = value;
	}
	
	public String valueToString() {
		StringBuilder buf = new StringBuilder();
		if (this.value != null) {
			for (String s : value) {
				buf.append(s);
			}
		}
		return buf.toString();
	}
	
	@Override
	public int compareTo(PairParams o) {
		if (key == null || key.length() < 1) {
			return -1;
		}
		if (o == null || o.key == null || o.key.length() < 1) {
			return 1;
		}
		return this.key.compareTo(o.key);
	}
	@Override
	public String toString() {
		return "[key=" + key + ", value=" + valueToString() + "]";
	}
	
}
